title: git文档 3-分支
date: 2020.5.17
top:
tags:


2020.5.24 星期日 22:56

  1. Git 分支

    3.1 分支简介

    Git 保存的不是文件的变化或者差异,而是一系列不同时刻的 快照 。

为了更加形象地说明,我们假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件。
Git 仓库中有五个对象:
三个 blob 对象(保存着文件快照)、
一个 树 对象 (记录着目录结构和 blob 对象索引)
一个 提交 对象(包含着指向前述树对象的指针和所有提交信息)。
图片解释
<!–
暂存操作会为每一个文件计算校验和(使用我们在 起步 中提到的 SHA-1 哈希算法),然后会把当前版本的文件快照保存到 Git 仓库中 (Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交

当使用 git commit 进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和, 然后在 Git 仓库中这些校验和保存为树对象。

随后,Git 便会创建一个提交对象, 它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。 如此一来,Git 就可以在需要的时候重现此次保存的快照。

做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。
–>

Figure 10. 提交对象及其父对象
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。

分支创建

Git 是怎么创建新分支的呢? 很简单,它只是为你创建了一个可以移动的新的指针。

Figure 12. 两个指向相同提交历史的分支
那么,Git 又是怎么知道当前在哪一个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。

Figure 13. HEAD 指向当前所在的分支
你可以简单地使用 git log 命令查看各个分支当前所指的对象。 提供这一功能的参数是 –decorate。

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) add feature #32 - ability to add new formats to the central interface
34ac2 Fixed bug #1328 - stack overflow under certain conditions
98ca9 The initial commit of my project

Figure 14. HEAD 指向当前所在的分支
Figure 15. HEAD 分支随着提交操作自动向前移动
Figure 16. 检出时 HEAD 随之移动
这条命令做了两件事。 一是使 HEAD 指回 master 分支,二是将工作目录恢复成 master 分支所指向的快照内容。

Note
分支切换会改变你工作目录中的文件
在切换分支时,一定要注意你工作目录里的文件会被改变。 如果是切换到一个较旧的分支,你的工作目录会恢复到该分支最后一次提交时的样子。 如果 Git 不能干净利落地完成这个任务,它将禁止切换分支

Figure 17. 项目分叉历史
$ git log --oneline --decorate --graph --all

由于 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符),如此的简单能不快吗?


这些高效的特性使得 Git 鼓励开发人员频繁地创建和使用分支。

3.2 分支的新建与合并

但是,在你这么做之前,要留意你的工作目录和暂存区里那些还没有被提交的修改, 它可能会和你即将检出的分支产生冲突从而阻止 Git 切换到该分支。
最好的方法是,在你切换分支之前,保持好一个干净的状态。
有一些方法可以绕过这个问题(即,暂存(stashing) 和 修补提交(commit amending)),

Figure 21. 基于 master 分支的紧急问题分支 hotfix branch
你可以运行你的测试,确保你的修改是正确的,然后将 hotfix 分支合并回你的 master 分支来部署到线上。
$_PS: 先测试,然后合并分支到master,最后部署线上

在合并的时候,你应该注意到了 “快进(fast-forward)”这个词。

Figure 22. master 被快进到 hotfix
关于这个紧急问题的解决方案发布之后,你准备回到被打断之前时的工作中。 然而,你应该先删除 hotfix 分支,因为你已经不再需要它了 —— master 分支已经指向了同一个位置。
$_PS: 不需要了,可以删除了(如果还需要再次修改,可以直接在这个这个分支上重新拉分支)。
$_PS: 那什么分支不可以删除呢? 回滚/发版/标签 分支吗

Figure 23. 继续在 iss53 分支上的工作

出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的公共祖先(C2),做一个简单的三方合并。

Figure 24. 一次典型合并中所用到的三个快照
和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。

遇到冲突时的分支合并
此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。

3.3 分支管理

# 需要查看每一个分支的最后一次提交
git branch -v
# --merged 与 --no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。
git branch --no-merged

# 你总是可以提供一个附加的参数来查看其它分支的合并状态而不必检出它们。 例如,尚未合并到 master 分支的有哪些?
git checkout testing
git branch --no-merged master ## 在testing 分支查看 没有merge到master 分支的分支

因为它包含了还未合并的工作,尝试使用 git branch -d 命令删除它时会失败:

3.4 分支开发工作流

介绍一些常见的利用分支进行开发的工作流程。

长期分支

在整个项目开发周期的不同阶段,你可以同时拥有多个开放的分支;你可以定期地把某些主题分支合并入其他分支中。

比如只在 master 分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码。
他们还有一些名为 develop 或者 next 的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。

Figure 26. 趋于稳定分支的线性图
再次强调一下,使用多个长期分支的方法并非必要,但是这么做通常很有帮助,尤其是当你在一个非常庞大或者复杂的项目中工作时。

主题分支

主题分支对任何规模的项目都适用。

在不同的流水线中每个分支都仅与其目标特性相关,因此,在做代码审查之类的工作的时候就能更加容易地看出你做了哪些改动。 你可以把做出的改动在主题分支中保留几分钟、几天甚至几个月,等它们成熟之后再合并,而不用在乎它们建立的顺序或工作进度。

Figure 29. 合并了 dumbidea 和 iss91v2 分支之后的提交历史
我们将会在 分布式 Git 中向你揭示更多有关分支工作流的细节, 因此,请确保你阅读完那个章节之后,再来决定你的下个项目要使用什么样的分支策略(branching scheme)。

请牢记,当你做这么多操作的时候,这些分支全部都存于本地。 当你新建和合并分支的时候,所有这一切都只发生在你本地的 Git 版本库中 —— 没有与服务器发生交互。

3.5 远程分支

远程引用是对远程仓库的引用(指针),包括分支、标签等等。

# 显式地获得远程引用的完整列表
git ls-remote <remote> 
# 获得远程分支的更多信息
git remote show <remote> 

然而,一个更常见的做法是利用远程跟踪分支

远程跟踪分支是远程分支状态的引用。

这就是说你们的提交历史已走向不同的方向。 即便这样,只要你保持不与 origin 服务器连接(并拉取数据),你的 origin/master 指针就不会移动。

Figure 31. 本地与远程的工作可以分叉
如果要与给定的远程仓库同步数据,运行 git fetch <remote> 命令(在本例中为 git fetch origin)。

从中抓取本地没有的数据,并且更新本地数据库,移动 origin/master 指针到更新之后的位置。

Figure 32. git fetch 更新你的远程跟踪分支
Figure 33. 添加另一个远程仓库
可以运行 git fetch teamone 来抓取远程仓库 teamone 有而本地没有的数据。
因为那台服务器上现有的数据是 origin 服务器上的一个子集, 所以 Git 并不会抓取数据而是会设置远程跟踪分支 teamone/master 指向 teamone 的 master 分支。

Figure 34. 远程跟踪分支 teamone/master

推送

本地的分支并不会自动与远程仓库同步——你必须显式地推送想要分享的分支。

# 推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支
$ git push origin serverfix
    #  Git 自动将 serverfix 分支名字展开为 refs/heads/serverfix:refs/heads/serverfix
# 它会做同样的事——也就是说“推送本地的 serverfix 分支,将其作为远程仓库的 serverfix 分支”
git push origin serverfix:awesomebranch
    # 推送本地分支到一个命名不相同的远程分支


# 如果不想在每一次推送时都输入用户名与密码,你可以设置一个 “credential cache”。 最简单的方式就是将其保存在内存中几分钟,
git config --global credential.helper cache

下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支 origin/serverfix,指向服务器的 serverfix 分支的引用:
git fetch origin
要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。
换一句话说,这种情况下,不会有一个新的 serverfix 分支——只有一个不可以修改的 origin/serverfix 指针。

可以运行 git merge origin/serverfix 将这些工作合并到当前所在的分支。
如果想要在自己的 serverfix 分支上工作,可以将其建立在远程跟踪分支之上:git checkout -b serverfix origin/serverfix
这会给你一个用于工作的本地分支,并且起点位于 origin/serverfix。

$_PS: 几个和orgin 相关的指令

git show orign
git push origin serverfix
git fetch orgin

git merge origin/serverfix
git checkout --track orgin/serverfix
    # git checkout -b <branch> <remote>/<branch> 的快捷方式
git checkout serverfix
    # 该捷径本身还有一个捷径。 如果你尝试检出的分支 (a) 不存在且 (b) 刚好只有一个名字与之匹配的远程分支,那么 Git 就会为你创建一个跟踪分支

跟踪分支

从一个远程跟踪分支检出一个本地分支会自动创建所谓的“跟踪分支”(它跟踪的分支叫做“上游分支”)。
跟踪分支是与远程分支有直接关系的本地分支。

当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/master 的 master 分支。

设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支, 你可以在任意时间使用 -u 或 --set-upstream-to 选项运行 git branch 来显式地设置。git branch -u origin/serverfix

拉取

当 git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并
然而,有一个命令叫作 git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。

$_PS: 这就是git fetch 和git pull 的区别比较的来源。(上述着重标出差异)

由于 git pull 的魔法经常令人困惑所以通常单独显式地使用 fetch 与 merge 命令会更好一些。

删除远程分支

可以运行带有 –delete 选项的 git push 命令来删除一个远程分支。
$ git push origin --delete serverfix

基本上这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。
$_PS: 远程分支上移除了指针。怎么再找回这个远程分支呢?
$_PS: 本地分支也删除了,怎么回复本地删除的分支呢?(git reflog)

01:09


2020.5.27 星期三 23:40

3.6 变基

2020.5.29 星期五 8:50

变基

在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。

$ git checkout experiment
$ git rebase master
# 现在回到 master 分支,进行一次快进合并。
$ git checkout experiment
$ git rebase master

它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master) 的最近共同祖先 C2,
然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件, 然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文
件的修改依序应用。

在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用
Figure 39. 从一个主题分支里再分出一个主题分支的提交历史

# 取出 client 分支,找出它从 server 分支分歧之后的补丁, 然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样
$ git rebase --onto master server client
$ git checkout master
$ git merge client

变基的风险

如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。

变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。

Figure 46. 有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交

Figure 47. 你将相同的内容又合并了一次,生成了一个新的提交
此时如果你执行 git log 命令,你会发现有两个提交的作者、日期、日志居然是一样的,这会令人感到混乱。
此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,

用变基解决变基

Figure 48. 在一个被变基然后强制推送的分支上再次执行变基
在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。 又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase teamone/master。

$_tips: 可以执行变基的情况
如果你只对不会离开你电脑的提交执行变基,那就不会有事。
如果你对已经推送过的提交执行变基,但别人没有基于它的提交,那么也不会有事。
如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交, 那你就有大麻烦了,。

请一定要通知每个人执行 git pull –rebase 命令,这样尽管不能避免伤痛,但能有所缓解。

变基 vs. 合并

现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。

3.7 总结